/**
 * 
 */
package map;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;

import map.model.Map;
import map.model.MapNode;
import map.model.MapWay;

/**
 * @author balage
 *
 */
public class NodeDataBase {

	public final List<NodeSlot> nodes = new ArrayList<NodeSlot>();
	
	public final HashMap<Long, Integer> noderefs = new HashMap<Long, Integer>(); //node id -> ref
	public final HashMap<Integer, Long> crossrefs = new HashMap<Integer, Long>(); //wayref -> node id
	
	public static final Comparator<MapNode> latcomparator = new LatNodeComparator();
	public static final Comparator<MapNode> loncomparator = new LonNodeComparator();
	
	public static NodeDataBase create(Map map){
		NodeDataBase db = new NodeDataBase();
		List<MapNode> mapnodes = map.nodes;
		
		System.out.println("Processing "+mapnodes.size()+" nodes and "+map.ways.size()+" ways.");
		
		int at = db.nodes.size();
		db.nodes.add(new NodeSlot());
		splitLatAndInsert(db, at, mapnodes,map);
		
		System.out.println("Resolving "+db.crossrefs.size()+" cross references..");
		for(Entry<Integer, Long> cref : db.crossrefs.entrySet()){
			NodeSlot wayslot = db.nodes.get(cref.getKey());
			if (wayslot == null)
				System.err.println("No such way slot: "+cref.getKey());
			else{
				Integer ref = db.noderefs.get(cref.getValue());
				if (ref == null)
					System.err.println("Can't resolve reference to node "+cref.getValue());
				else
					wayslot.l2 = ref;
			}
		}
		
		return db;
	}
	
	private static void splitLatAndInsert(NodeDataBase db, int at, List<MapNode> what,Map map){
		if (what.size() < 5){
			insertUnsplitted(db, at, what,map);
			return;
		}
		Collections.sort(what,latcomparator);
		
		int i = what.size()/2;
		int j = i-1;
		
		double lat = (what.get(i).lat + what.get(j).lat)/2;
		
		NodeSlot slot = db.nodes.get(at);
		slot.type = NodeSlot.TYPE_SPLIT_LAT;
		slot.id = Double.doubleToLongBits(lat);
		slot.w = -1;
		
		int l1 = db.nodes.size();
		db.nodes.add(new NodeSlot());
		slot.l1 = l1;
		
		splitLonAndInsert(db, l1, new ArrayList<MapNode>( what.subList(0, i)),map);
		
		l1 = db.nodes.size();
		db.nodes.add(new NodeSlot());
		slot.l2 = l1;
		
		splitLonAndInsert(db, l1, new ArrayList<MapNode>(what.subList(i, what.size())),map);
	}
	
	private static void splitLonAndInsert(NodeDataBase db, int at, List<MapNode> what, Map map){
		if (what.size() < 5){
			insertUnsplitted(db, at, what,map);
			return;
		}
		Collections.sort(what,loncomparator);
		
		int i = what.size()/2;
		int j = i-1;
		
		double lon = (what.get(i).lon + what.get(j).lon)/2;
		
		NodeSlot slot = db.nodes.get(at);
		slot.type = NodeSlot.TYPE_SPLIT_LON;
		slot.id = Double.doubleToLongBits(lon);
		slot.w = -1;
		
		int l1 = db.nodes.size();
		db.nodes.add(new NodeSlot());
		slot.l1 = l1;
		
		splitLatAndInsert(db, l1, what.subList(0, i),map);
		
		l1 = db.nodes.size();
		db.nodes.add(new NodeSlot());
		slot.l2 = l1;
		
		splitLatAndInsert(db, l1, what.subList(i, what.size()),map);
	}
	
	private static void insertUnsplitted(NodeDataBase db, int at, List<MapNode> what, Map map){
		if (what.size() == 1){
			insertANode(db, at, what.get(0),map);
			return;
		}
		
		NodeSlot slot = db.nodes.get(at);
		slot.type = NodeSlot.TYPE_LIST;
		slot.id = Long.MAX_VALUE;
		slot.l1 = Long.MAX_VALUE;
		slot.l2 = Long.MAX_VALUE;
		slot.w = Long.MAX_VALUE;
		
		if (what.size() == 0)
			return;
		
		int newat = db.nodes.size();
		db.nodes.add(new NodeSlot());
		slot.id = newat;
		insertANode(db, newat, what.get(0),map);
		
		newat = db.nodes.size();
		db.nodes.add(new NodeSlot());
		slot.l1 = newat;
		insertANode(db, newat, what.get(1),map);
		
		if (what.size() > 2){
			newat = db.nodes.size();
			db.nodes.add(new NodeSlot());
			slot.l2 = newat;
			insertANode(db, newat, what.get(2),map);
			
			if (what.size() > 3){
				newat = db.nodes.size();
				db.nodes.add(new NodeSlot());
				slot.w = newat;
				insertUnsplitted(db, newat, what.subList(3, what.size()),map);
			}
		}
		
	}
	
	private static void insertANode(NodeDataBase db, int at, MapNode what, Map map){
		NodeSlot slot = db.nodes.get(at);
		slot.type = NodeSlot.TYPE_NODE;
		slot.id = what.id;
		slot.l1 = Double.doubleToLongBits(what.lat);
		slot.l2 = Double.doubleToLongBits(what.lon);
		slot.w = Long.MAX_VALUE;
		db.noderefs.put(what.id, at);
	
		for(MapWay w : map.ways){
			for(int i=0;i<w.nodes.size()-1;i++){
				if (w.nodes.get(i).longValue() == what.id){
					slot.w = db.nodes.size();
					NodeSlot wslot = new NodeSlot();
					db.nodes.add(wslot);
					wslot.type = NodeSlot.TYPE_WAY;
					wslot.id = w.id;
					wslot.l1 = w.getWayType();
					long crossref = w.nodes.get(i+1).longValue();
					db.crossrefs.put((int)slot.w,crossref);
					wslot.l2 = 0;//cross reference resolved after populating database
					wslot.w = Long.MAX_VALUE;
					slot = wslot;
				}
			}
		}
		
	}
	
	public void save(File file) throws IOException{
		FileOutputStream os = new FileOutputStream(file);
		DataOutputStream dos = new DataOutputStream(os);
		for(NodeSlot ns : nodes){
			dos.writeByte(ns.type);
			dos.writeLong(ns.id);
			dos.writeLong(ns.l1);
			dos.writeLong(ns.l2);
			dos.writeLong(ns.w);
		}
		dos.close();
		os.close();
	}
	
}
